package com.fb.cvstool.netbeans;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;

import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.netbeans.lib.cvsclient.CVSRoot;
import org.netbeans.lib.cvsclient.Client;
import org.netbeans.lib.cvsclient.admin.StandardAdminHandler;
import org.netbeans.lib.cvsclient.command.CommandAbortedException;
import org.netbeans.lib.cvsclient.command.CommandException;
import org.netbeans.lib.cvsclient.command.FileInfoContainer;
import org.netbeans.lib.cvsclient.command.GlobalOptions;
import org.netbeans.lib.cvsclient.command.KeywordSubstitutionOptions;
import org.netbeans.lib.cvsclient.command.add.AddCommand;
import org.netbeans.lib.cvsclient.command.checkout.CheckoutCommand;
import org.netbeans.lib.cvsclient.command.commit.CommitCommand;
import org.netbeans.lib.cvsclient.command.remove.RemoveCommand;
import org.netbeans.lib.cvsclient.command.status.StatusCommand;
import org.netbeans.lib.cvsclient.command.status.StatusInformation;
import org.netbeans.lib.cvsclient.command.update.UpdateCommand;
import org.netbeans.lib.cvsclient.connection.AuthenticationException;
import org.netbeans.lib.cvsclient.connection.Connection;
import org.netbeans.lib.cvsclient.connection.ConnectionFactory;
import org.netbeans.lib.cvsclient.event.CVSAdapter;
import org.netbeans.lib.cvsclient.event.FileInfoEvent;
import org.netbeans.lib.cvsclient.event.MessageEvent;
import org.netbeans.lib.cvsclient.event.TerminationEvent;
import org.netbeans.lib.cvsclient.file.FileStatus;

/**
 * The main way of communication with a server using the CVS Protocol.
 * Connection to the server is also encapsulated inside this classs. Relevant
 * CVS operations are enabled by instantiation of this class. Currently protocol
 * of pserver, server and ssh/extssh are supported.
 * 
 * @author <a href="mailto:babala_234@163.com">Micheal Hong</a>
 * @version 1.4 2008-12-22 change constructor from CVSClient(String label,String
 *          cvsRoot,String localPath) to CVSClient(String label,String
 *          localPath) version 1.2 2008-12-9 1.change function restoreFile()'s
 *          name to restoreModifiedFile() 2.add function restoreRemovedFile()
 *          3.add function getStatus() version 1.0 2008-12-5
 */
public class NetBeansCVSClient {

	private GlobalOptions options;
	private BasicListener listener;
	private String label = null;
	private String localPath = null;
	private boolean performSuccessed;
	private int status = -1;
	private Logger logger;

	/* Status of File */
	public static final int STATUS_UP_TO_DATE = 0x02;
	public static final int STATUS_LOCALLY_MODIFIED = 0x04;
	public static final int STATUS_LOCALLY_ADDED = 0x08;
	public static final int STATUS_LOCALLY_REMOVED = 0x16;
	public static final int STATUS_NEEDS_CHECKOUT = 0x32;
	public static final int STATUS_NEEDS_PATCH = 0x64;
	public static final int STATUS_NEEDS_MERGE = 0x128;
	public static final int STATUS_UNRESOLVED_CONFLICT = 0x256;
	public static final int STATUS_INVALID = 0x512;
	public static final int STATUS_UNKNOWN = 0x1024;
	public static final int STATUS_HAS_CONFLICTS = 0x048;

	/**
	 * Instantiate the class.
	 * 
	 * @param label
	 *            a string must be of the form
	 *            [:method:][[user][:password]@][hostname
	 *            :[port]]/path/to/repository
	 * @param localPath
	 *            the path to the directory in which the currently executing
	 *            command is referring.
	 */
	public NetBeansCVSClient(String label, String localPath) {
		// PropertyConfigurator.configure("../properties/log4j.properties");
		// this.logger = Logger.getLogger("cvsLogger");
		this.label = label;
		this.localPath = new File(localPath).getAbsolutePath();
		System.out.println(this.localPath);
		this.listener = new BasicListener();
		options = new GlobalOptions();
		options.setCVSRoot(CVSRoot.parse(label).getRepository());
	}

	/**
	 * Reinitiate connection and client for the reuse of the them is not
	 * recommenmded.
	 * 
	 * @return a new client
	 * @throws CommandAbortedException
	 *             if the connection is aborted during execution
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 */
	private Client reinitiateClient() throws CommandAbortedException,
			AuthenticationException {
		CVSRoot cvsRoot = CVSRoot.parse(label);
		Connection conn = ConnectionFactory.getConnection(cvsRoot);
		// logger.debug("CVSRoot[" + cvsRoot.getMethod() + ":"
		// + cvsRoot.getHostName() + ":" + cvsRoot.getPassword() + ":"
		// + cvsRoot.getPort() + ":" + cvsRoot.getRepository() + "]");
		Client client = new Client(conn, new StandardAdminHandler());
		client.setLocalPath(localPath);
		client.getEventManager().addCVSListener(listener);
		conn.open();
		return client;
	}

	/**
	 * Check for status of the file in the repository
	 * 
	 * @param file
	 *            file path of the file to be checked for status. The existence
	 *            of the file is not demanded. The path is a relative path
	 *            relative to the cvs root. <br>
	 *            Note that the path must be started with a slash "/".
	 * @return status of the file
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public int checkStatus(String file) throws CommandAbortedException,
			CommandException, AuthenticationException, IOException {

		System.out.println("  check status file: " + localPath + file);
		return checkStatus(new File(localPath + file));

	}

	/**
	 * Check for status of the file in the repository
	 * 
	 * @param file
	 *            the file to be checked for status. The existence of the file
	 *            is not demanded.
	 * @return status of the file
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public int checkStatus(File file) throws CommandAbortedException,
			CommandException, AuthenticationException, IOException {
		Client client = reinitiateClient();
		File[] files = new File[1];
		files[0] = file;
		StatusCommand command = new StatusCommand();
		command.setFiles(files);
		client.executeCommand(command, options);
		client.getConnection().close();
		return status;
	}

	/**
	 * Export the specific modules from the CVS server.
	 * 
	 * @param path
	 *            path of the modules to be exported. The path is a relative
	 *            path relative to the cvsRoot. <br>
	 *            Note that the path must not be started with a slash "/".
	 * @return true if success in export performance,or false otherwise.
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public boolean checkout(String path) throws CommandAbortedException,
			CommandException, AuthenticationException, IOException {
		Client client = reinitiateClient();
		CheckoutCommand command = new CheckoutCommand();
		command.setModule(path);
		performSuccessed = true;
		client.executeCommand(command, options);
		client.getConnection().close();
		return performSuccessed;
	}

	/**
	 * Adds a file or directory.It is treated as a binary file to check in.
	 * 
	 * @param file
	 *            path of the file of directory to be added. The path is a
	 *            relative path relative to the cvs root. <br>
	 *            Note that the path must be started with a slash "/".
	 * @return true if success in addition performance,or false otherwise.
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public boolean addFile(String file) throws CommandAbortedException,
			CommandException, AuthenticationException, IOException {
		return addFile(new File(localPath + file));
	}

	/**
	 * Adds a file or directory.It is treated as a binary file to check in.
	 * 
	 * @param file
	 *            the file of directory to be added.
	 * @return true if success in addition performance,or false otherwise.
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public boolean addFile(File file) throws CommandAbortedException,
			CommandException, AuthenticationException, IOException {
		Client client = reinitiateClient();
		File[] files = new File[1];
		files[0] = file;
		AddCommand command = new AddCommand();
		command.setFiles(files);
		/* the specified filename should be treated as a binary file. */
		command.setKeywordSubst(KeywordSubstitutionOptions
				.findKeywordSubstOption("b"));
		performSuccessed = true;
		client.executeCommand(command, options);
		client.getConnection().close();
		return performSuccessed;
	}

	/**
	 * Remove file or directory from the repository.
	 * 
	 * @param file
	 *            path of the file or directory to be removed. The path is a
	 *            relative path relative to the cvs root. <br>
	 *            Note that the path must be started with a slash "/".
	 * @return true if success in removing performance,or false otherwise.
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public boolean removeFile(String file) throws CommandAbortedException,
			CommandException, AuthenticationException, IOException {
		return removeFile(new File(localPath + file));
	}

	/**
	 * Remove file or directory from the repository.
	 * 
	 * @param file
	 *            the file or directory to be removed.
	 * @return true if success in removing performance,or false otherwise.
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public boolean removeFile(File file) throws CommandAbortedException,
			CommandException, AuthenticationException, IOException {
		Client client = reinitiateClient();
		File[] files = new File[1];
		files[0] = file;
		RemoveCommand command = new RemoveCommand();
		command.setFiles(files);
		command.setIgnoreLocallyExistingFiles(true);// Sets whether locally
		// existing files will be
		// treated as they were
		// deleted before.
		performSuccessed = true;
		client.executeCommand(command, options);
		client.getConnection().close();
		return performSuccessed;
	}

	/**
	 * Commit any changes of the file that have been made.
	 * 
	 * @param file
	 *            file path of the file to be committed. The path is a relative
	 *            path relative to the cvs root. <br>
	 *            Note that the path must be started with a slash "/".
	 * @return true if success in commitment performance,or false otherwise.
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public boolean commitFile(String file) throws CommandAbortedException,
			CommandException, AuthenticationException, IOException {
		return commitFile(new File(localPath + file));
	}

	/**
	 * Commit any changes of the file that have been made.
	 * 
	 * @param file
	 *            the file to be committed.
	 * @return true if success in commitment performance,or false otherwise.
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public boolean commitFile(File file) throws CommandAbortedException,
			CommandException, AuthenticationException, IOException {
		Client client = reinitiateClient();
		File[] files = new File[1];
		files[0] = file;
		CommitCommand command = new CommitCommand();
		command.setFiles(files);
		performSuccessed = true;
		client.executeCommand(command, options);
		client.getConnection().close();
		return performSuccessed;
	}

	/**
	 * Updates files that have previously been checked out from the repository
	 * with the checkout command. Modified files are not overwritten.
	 * 
	 * @param file
	 *            file path of the file to be updated. The path is a relative
	 *            path relative to the cvs root. <br>
	 *            Note that the path must be started with a slash "/".
	 * @return true if success in update performance,or false otherwise.
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public boolean updateFile(String file) throws CommandAbortedException,
			CommandException, AuthenticationException, IOException {
		return updateFile(new File(localPath + file));
	}

	/**
	 * Updates files that have previously been checked out from the repository
	 * with the checkout command. Modified files are not overwritten.
	 * 
	 * @param file
	 *            the file to be updated.
	 * @return true if success in update performance,or false otherwise.
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public boolean updateFile(File file) throws CommandAbortedException,
			CommandException, AuthenticationException, IOException {
		Client client = reinitiateClient();
		File[] files = new File[1];
		files[0] = file;
		UpdateCommand command = new UpdateCommand();
		command.setBuilder(null);
		command.setFiles(files);
		command.setRecursive(true);
		command.setBuildDirectories(true);
		command.setPruneDirectories(true);
		performSuccessed = true;
		client.executeCommand(command, options);
		client.getConnection().close();
		return performSuccessed;
	}

	/**
	 * Restore file that has been modified to the latest revision.
	 * 
	 * @param file
	 *            file file path of the file to be restored. The path is a
	 *            relative path relative to the cvs root. <br>
	 *            Note that the path must be started with a slash "/".
	 * @return true if success in restore performance,or false otherwise.
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public boolean restoreModifiedFile(String file)
			throws CommandAbortedException, CommandException,
			AuthenticationException, IOException {
		return restoreModifiedFile(new File(localPath + file));
	}

	/**
	 * Restore file that has been modified to the latest revision.
	 * 
	 * @param file
	 *            the file to be restored.
	 * @return true if success in restore performance,or false otherwise.
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public boolean restoreModifiedFile(File file)
			throws CommandAbortedException, CommandException,
			AuthenticationException, IOException {
		/*
		 * Originally,it is supposed to use command "update -C <file>" to get a
		 * clean copy of the file from the server, even the file has been
		 * modified, but it fails, reported as a bug in the page
		 * http://savannah.nongnu.org/bugs/?19528#postcomment. It may be solve
		 * by appling a patch following the page:
		 * http://savannah.nongnu.org/file/conflict.patch?file_id=15643, but it
		 * is hard to reach. So we take a temporary measures that delete the
		 * file first and then update it from the server.
		 */
		Client client = reinitiateClient();
		File[] files = new File[1];
		files[0] = file;
		file.delete();
		UpdateCommand command = new UpdateCommand();
		command.setFiles(files);
		// command.setRecursive(false);
		// command.setCleanCopy(true);
		// command.setResetStickyOnes(true);
		performSuccessed = true;
		client.executeCommand(command, options);
		client.getConnection().close();
		return performSuccessed;
	}

	/**
	 * Restore file that has been removed but before been committed to the
	 * server.
	 * 
	 * @param file
	 *            file file path of the file to be restored. The path is a
	 *            relative path relative to the cvs root. <br>
	 *            Note that the path must be started with a slash "/".
	 * @return true if success in restore performance,or false otherwise.
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public boolean restoreRemovedFile(String file)
			throws CommandAbortedException, CommandException,
			AuthenticationException, IOException {
		return restoreRemovedFile(new File(localPath + file));
	}

	/**
	 * Restore file that has been removed but before been committed to the
	 * server.
	 * 
	 * @param file
	 *            the file to be restored.
	 * @return true if success in restore performance,or false otherwise.
	 * @throws CommandAbortedException
	 *             if the connection or the command is aborted during execution
	 * @throws CommandException
	 *             if an error occurs while executing the command
	 * @throws AuthenticationException
	 *             if the connection with the server cannot be established
	 * @throws IOException
	 *             if fail to close the connection
	 */
	public boolean restoreRemovedFile(File file)
			throws CommandAbortedException, CommandException,
			AuthenticationException, IOException {
		performSuccessed = addFile(file);
		if (performSuccessed == true) {
			performSuccessed = commitFile(file);
			if (performSuccessed == true) {
				performSuccessed = addFile(file);
				if (performSuccessed == true) {
					performSuccessed = commitFile(file);
				}
			}
		}
		return performSuccessed;
	}

	/**
	 * Get status string from status constant.
	 * 
	 * @param status
	 *            status constant inside CVSClient
	 * @return status string corresponding to status constant if exist, null
	 *         otherwise.
	 */
	public static String getStatus(int status) {
		switch (status) {
		case STATUS_UP_TO_DATE:
			return "Up-to-date";
		case STATUS_LOCALLY_MODIFIED:
			return "Locally Modified";
		case STATUS_LOCALLY_ADDED:
			return "Locally Added";
		case STATUS_LOCALLY_REMOVED:
			return "Locally Removed";
		case STATUS_NEEDS_CHECKOUT:
			return "Needs Checkout";
		case STATUS_NEEDS_PATCH:
			return "Needs Patch";
		case STATUS_NEEDS_MERGE:
			return "Needs Merge";
		case STATUS_UNRESOLVED_CONFLICT:
			return "Unresolved Conflict";
		case STATUS_INVALID:
			return "Entry Invalid";
		case STATUS_UNKNOWN:
			return "Unknown";
		case STATUS_HAS_CONFLICTS:
			return "File had conflicts on merge";
		default:
			return null;
		}
	}

	class BasicListener extends CVSAdapter {

		/** Stores a tagged line */
		private final StringBuffer taggedLine = new StringBuffer();

		/**
		 * Called when the server wants to send a message to be displayed to the
		 * user. The message is only for information purposes and clients can
		 * choose to ignore these messages if they wish.
		 * 
		 * @param e
		 *            the event
		 */
		public void messageSent(MessageEvent e) {
			String line = e.getMessage();
			PrintStream stream = e.isError() ? System.err : System.out;

			if (e.isTagged()) {
				String message = MessageEvent.parseTaggedMessage(taggedLine, e
						.getMessage());
				if (message != null) {
					stream.println(message);
				}
			} else {
				stream.println(line);
			}
		}

		/**
		 * Called when file status information has been received
		 */
		public void fileInfoGenerated(FileInfoEvent e) {
			FileInfoContainer fileInfo = e.getInfoContainer();
			if (fileInfo.getClass().equals(StatusInformation.class)) {
				// System.err.println("A file status event was received.");
				// System.err.println("The status information object is: " +
				// fileInfo);
				// System.out.println(FileStatus.getStatusForString(((StatusInformation)fileInfo).getStatusString()));
				// System.out.println(((StatusInformation)fileInfo).getStatus().equals(FileStatus.UP_TO_DATE));
				FileStatus fileStatus = ((StatusInformation) fileInfo)
						.getStatus();
				if (fileStatus.equals(FileStatus.UP_TO_DATE)) {
					status = STATUS_UP_TO_DATE;
				} else if (fileStatus.equals(FileStatus.NEEDS_CHECKOUT)) {
					status = STATUS_NEEDS_CHECKOUT;
				} else if (fileStatus.equals(FileStatus.NEEDS_MERGE)) {
					status = STATUS_NEEDS_MERGE;
				} else if (fileStatus.equals(FileStatus.NEEDS_PATCH)) {
					status = STATUS_NEEDS_PATCH;
				} else if (fileStatus.equals(FileStatus.UNKNOWN)) {
					status = STATUS_UNKNOWN;
				} else if (fileStatus.equals(FileStatus.UNRESOLVED_CONFLICT)) {
					status = STATUS_UNRESOLVED_CONFLICT;
				} else if (fileStatus.equals(FileStatus.ADDED)) {
					status = STATUS_LOCALLY_ADDED;
				} else if (fileStatus.equals(FileStatus.MODIFIED)) {
					status = STATUS_LOCALLY_MODIFIED;
				} else if (fileStatus.equals(FileStatus.REMOVED)) {
					status = STATUS_LOCALLY_REMOVED;
				} else if (fileStatus.equals(FileStatus.INVALID)) {
					status = STATUS_INVALID;
				} else if (fileStatus.equals(FileStatus.HAS_CONFLICTS)) {
					status = STATUS_HAS_CONFLICTS;
				} else {
					status = -1;
				}
			}
		}

		public void commandTerminated(TerminationEvent e) {
			if (e.isError()) {
				System.out.println("commandTerminated_Error:" + e);
				performSuccessed = false;
			}
			// else {
			// System.out.println("commandTerminated:"+e+"   e.getSource():"+((Response)(e.getSource())).isTerminalResponse());
			// }
		}

		// public void moduleExpanded(ModuleExpansionEvent e) {
		// System.out.println("moduleExpanded:"+e);
		// }
		// public void fileAdded(FileAddedEvent e) {
		// System.out.println("fileAdded:"+e);
		// }
		// public void fileToRemove(Object e) {
		// System.out.println("fileToRemove:"+e);
		//
		// }
		// public void fileRemoved(FileRemovedEvent e) {
		// System.out.println("fileRemoved:"+e);
		// }
		// public void fileUpdated(FileUpdatedEvent e) {
		// System.out.println("fileUpdated:"+e);
		// }
	}

	public String getLabel() {
		return label;
	}

	public String getLocalPath() {
		return localPath;
	}

}